-
История одной оптимизации
Летом мы запустили новый купонный проект BigBuzzy. Таких проектов к тому времени было довольно много и чтобы выделиться из толпы мы решили немного поменять бизнес-модель: вместо одного предложения в день выдавать четыре. Но, как это обычно бывает, аппетит приходит во время еды, поэтому уже спустя несколько месяцев на главной странице красовалось не 4, а 30 предложений.
И мы сразу же начали получать жалобы о жутких тормозах на главной странице. На поиск и устранение проблем у меня ушло два дня. О том, как находились узкие места и будет сегодняшний рассказ. А заодно научимся пользоваться инструментами вроде Web Inspector’s Timeline (если вы их ещё не освоили).
Поиск проблемы
Итак, мы столкнулись с фактом, что наша главная страница тормозит. Источник проблем был найден сразу: это анимированные таймеры у каждого предложения:
Самые большие проблемы наблюдались в Firefox: загрузка процессора на главной странице доходила до 70%. Поэтому я начал рассматривать скрипт таймера под микроскопом, а именно в Web Inspector, который по умолчанию входит в состав браузеров Safari и Chrome. Вообще, многие ребята довольно снисходительно относятся к этому инструменту, продолжая по привычке работать в Firebug’е, а зря. Лично для меня Web Inspector стал основным инструментом для отладки: выглядит он приятнее и содержит ряд полезных нововведений.
Исследуем узкие места
Так как сам скрипт таймера довольно простой, то не было смысла заниматься его профилированием — проблема явно где-то в reflow и repaint. Поэтому скрипт нужно исследовать через Timeline:
Полагаю, что многие читатели ещё ни разу не сталкивались с этим инструментом, поэтому принцип его работы и поиска проблем опишу в небольшом уроке. Стоит отметить, что Web Inspector в Chrome немного круче, чем в Safari, поэтому рекомендую пользоваться первым браузером.
Timeline показывает нам практически все процессы, которые происходят в браузере: запуск скрипта, отработка события, перерисовка экрана, установка таймера, отправка аякс-запроса и т.д. Так как данных довольно много и в них легко запутаться, я рекомендую изолировать исследуемый скрипт — выделить его в отдельную страницу. Для практики можно начать с простого шаблона, на котором мы будем экспериментировать.
Открываем шаблон в браузере и запускаем Web Inspector, вкладка Timeline. На странице есть красный квадратик и кнопка «Test». Чтобы начать исследование, нужно нажать на кнопку записи вкладки Timeline , а потом нажать на кнопку «Test» в основном окне браузера. Наш квадратик посинел и стал больше по высоте, а в Timeline записались следующие события:
Первые три записи относятся непосредственно к кнопке, которую нажали: применили псевдо-класс
:active
(Recalculate style), отобразили изменения на экране (Paint), вернули кнопку в исходное состояние, убрав:active
(Recalculate style). После того, как пользователь отпустил кнопку мышки сработало событиеclick
, и именно оно и всё, что ниже, нас будет интересовать.Во время клика сработал следующий скрипт:
function test() { var el = document.getElementById('test'); el.style.backgroundColor = 'blue'; el.style.height = '100px'; }
Ничего особенного: просто получили ссылку на элемент и поменяли у него цвет фона и высоту. Этот процесс был отображён на временной шкале: пересчитали стили (Recalculate style), пересчитали геометрию объектов (Layout) и отобразили изменения (Paint).
Как видите, несмотря на то, что мы поменяли два СSS-свойства, пересчёт стилей произошёл всего один раз. Поменяем скрипт:
function test() { el.style.backgroundColor = 'blue'; var height = el.offsetHeight; el.style.width = '100px'; }
Между присваиванием новых стилей мы решили получить высоту объекта. Но временная шкала сильно преобразилась:
Теперь у нас уже два события Recalculate style, а у самого события
click
появилась группировка (треугольник слева от жёлтой полоски), которая указывает, какие именно события произошли во время клика.Этот небольшой пример указывает на две очень важные особенности браузеров — это откладывание перерисовки на момент выхода из функции (первый пример) и существование определённых свойств у элемента, которые принудительно вызывают пересчёт стилей (далее restyle; второй пример). О существовании особых свойств, вызывающих restyle, думаю, многие уже знали: это свойства вроде
offsetLeft/Right/Width/Height
,clientLeft/Right/Width/Height
и так далее. Во втором примере, после установки свойстваbackgroundColor
браузер пометил дерево элементов как требующего пересчёта стилей. А обращение кoffsetHeight
принудительно вызвало этот пересчёт. Затем мы установили свойствоwidth
, которое отложило пересчёт стилей, геометрии и отображения на момент выхода из потока JS-функций.Отсюда первое правило: нужно стараться не смешивать получение и запись CSS-свойств. Лучше, например, сначала получить нужные свойства элемента, а затем присвоить новые.
Для любителей jQuery более красноречивым будет вот такой пример:
function test() { var e = $('#test'); var width = e.css('width'); if (width == '50px') e.css('width', '100px'); var height = e.css('height'); if (height == '50px') e.css('height', '100px'); }
Вот его шкала:
Как видите, помимо лишнего Recalculate style появился Layout (reflow), что сделало выполнение скрипта более медленным. Если немного оптимизировать, переместив получение высоты выше в коде:
function test() { var e = $('#test'); var width = e.css('width'), height = e.css('height'); if (width == '50px') e.css('width', '100px'); if (height == '50px') e.css('height', '100px'); }
…получим совершенно иную картину:
Лишний Layout (помимо Recalculate style) объясняется тем, что jQuery каждый раз при получении CSS-свойств вызывал
window.getComputedStyle()
, который принудительно запускает reflow. Справедливости ради стоит отметить, что в функцииjQuery.css()
есть оптимизация, которая сначала проверяет наличие запрашиваемого свойства вelement.style
и если его там нет, вызываетwindow.getComputedStyle()
. Но в любом случае, лучше всегда разделять чтение и изменение свойств.Таймеры
Напомню, что я занимался оптимизацией таймеров, которых было несколько. И каждый таймер работает через свой
setTimeout()
. Посмотрим, что это означает на практике:function test() { var el = document.getElementById('test'); setTimeout(function(){ el.style.backgroundColor = 'blue'; }, 10); setTimeout(function(){ el.style.width = '100px'; }, 10); }
У обоих таймеров одинаковый период ожидания и момент исполнения. На шкале видно, что после каждого таймера был запущен пересчёт стилей. Но в реальности момент исполнения будет далеко не всегда одинаковым. Поэтому поменяем задержку у последнего таймера — поставим 11 мс вместо 10 мс:
function test() { var el = document.getElementById('test'); setTimeout(function(){ el.style.backgroundColor = 'blue'; }, 10); setTimeout(function(){ el.style.width = '100px'; }, 11); }
И мы видим, что на шкале появился дополнительный repaint:
В итоге получалось, что из-за нескольких запущенных
setTimeout()
срабатывали ненужные перерисовки, которые пользователь всё равно не увидит, но процессор это нагружало прилично. Я переписал код таймеров таким образом, чтобы всё работало через один глобальный таймаут.Итак, суммируя всё вышесказанное, для оптимизации я
- разделил чтение и запись CSS-свойств;
- дополнительно сделал кэширование текущих значений анимации, чтобы меньше обращаться к элементам;
- заменил несколько таймаутов на один.
В итоге в Firefox нагрузка на процессор снизилась… всего на 10%. Вообще, это было крайне странно: даже при наличии всего одного анимированного таймера на странице Firefox грузил процессор на 60%, при том что Webkit грузил всего на 5%. Нужно копать дальше.
Влияние вёрстки на производительность
Два года назад я делал большое исследование на тему того, как вёрстка влияет на производительность браузера. Похоже, у моей проблемы схожие корни, поэтому начал перебирать все известные мне варианты оптимизации. Но, к сожалению, ничего не помогало.
Так как все restyle и reflow процессы я оптимизировал, проблема явно была где-то в repaint. Вспомнил, что в Firefox 3.5 появилось событие mozAfterRepaint, которое позволяет увидеть области, которые были перерисованы во время repaint. Для удобства было поставлено расширение Firebug Paint Events, которое позволяет отслеживать перерисовки экрана.
Чтобы описать всю бурю эмоций, которые я испытал после просмотра логов, предлагаю читателю посмотреть на скриншот, где указана область перерисовки во время работы всего одного таймера на странице:
Я специально оставил только 8 из 30 предложений, чтобы картинка не распирала страницу, но смысл, думаю, ясен: во время анимации даже одного таймера перерисовывалось примерно 90% страницы, 15 раз в секунду. И это при условии, что у цифр таймера указан
position:absolute
, а у их контейнераoverflow:hidden
. То есть сама анимация по определению никак не могла повлиять на области вне контейнера (на скриншоте обозначен синим прямоугольником), но перерисовывалась почти вся страница.Около часа мне понадобилось на то, чтобы найти причину такого странного поведения. Ей оказалось… свойство
float:left
у одного из контейнеров. Как только я заменял его наfloat:none
нагрузка на процессор падала ниже 10% (сfloat:left
была около 60%).Проблема проявляется стабильно, причём не только в Firefox, но и в Opera и IE8. Я сделал простую демку, где можно в живую увидеть эту проблему. В ней всего несколько блоков, однако у них указан
box-shadow
— очень тяжелое в плане нагрузки на процессор CSS-свойство. В правом верхнем углу есть кнопка, которая всего лишь переключаетfloat
у контейнера. Понаблюдайте за нагрузкой на процессор при разных состояниях кнопки, а также за областью перерисовки.В общем виде проблему можно описать так:
Repaint срабатывает на контейнере самого дальнего родителя, у которого указан
float:left|right
.Схематично это выглядит так:
Причём проблема не только во
float
. Я перепробовал различные варианты горизонатльной группировки блоков:display:inline-block
,display:table-cell
, таблицы и даже новомодные flex box — во всех случаях проблема оставалось. Помогало только абсолютное позиционирование боковых блоков.В общем виде я проблему решил: поставил боковую панель в коде перед основным контейнером и только ей указал
float
. Основной контейнер был безfloat
и repaint происходил именно там, где нужно. Однако на живом сайте решить проблему не удалось, так как на большинстве страниц стояли clearfix-элементы, из-за которых макет разваливался. Поэтому пришлось пока отключить анимацию с таймеров 🙁***
Честно говоря, после таких браузерных крендебобелей на всякие пузомерки типа Peackeeper, которыми так хвастаются разработчики с каждым новым релизом своего браузера, без слёз смотреть не получается. Поэтому мой вам совет: заранее узнавайте о всех интерактивных элементах на странице, не увлекайтесь новым CSS3, продумывайте рост сайта заранее и пользуйтесь правильными инструментами для отладки производительности — тогда будет вам счастье и высокая производительность.
Если хотите узнать больше о профилировании производительности, очень рекомендую статью Стояна Стефанова на эту тему.
40 комментариев
Спасибо за статью, очень познавательно.
А зачем было ставить setTimeout на 10мс? 1000мс более чем достаточно для данной задачи — конечно может быть небольшое отставание, но для данной задачи оно не важно.
Или же в setTimout рисовалась анимация? Ну по-моему, пусть лучше уж jQuery этим занимается, тем более, что она сама организует единый setTimeout на всю анимацию.
Кстати, хорошо что убрал анмиацию. Для подобной фоновой анимации slide-эффект не подходит — слишком отвлекает. Тут лучше fade (он отвлекает даже меньше, чем без анимации). Но fade имеет смысл на картинках и больших вещах, а не на маленьких цифрах.
Меня что смущает в анимации — при определённых ситуация в WebKit есть проблемы: Safari выключает сглаживание шрифта, а Chrome оставляет шлейф от букв в один пиксель длинной перед самым концом анимации. Тоже наверняка связано с repaint.
Поставил багу с картинками и примерами в наш трекер, думаю соответствующие люди заинтересуются. Вот только удивительно почему браузеры столь одинаково реагируют на такую парадоксальную вещь. Те же Opera и Firefox вырастают примерно в 5 раз от состояния покоя по CPU.
ps: твой WP-Spamfree идиот и дважды отказал мне наличии куков и JS в браузере (Safari 5)
А если таймеры показывать из iframe?
Отличная статья, спасибо! Я увлекаюсь написанием всяких скриптов — буду знать, куда копать.
Спасибо за статью, интересно, наглядно и познавательно, но как-то правда бесперспективно (( как то ждал серебряной пули в конце, а тут… да еще и во всех современных, но хорошо что замечено и проанализировано, кто предупрежден тот вооружен.
Я правильно понял, что имеется ввиду не просто применение CSS3, а его применение в тех местах где может происходить его repaint?
О Chrome Developers Tool, которую разрабатывают в Петербурге, рассказывал один из руководителей его разработки на GDD. Действительно мощный инструмент, многих вещей нет ни в одном браузере, и предвидятся ещё больше крутых штук.
В одной из своих заметок о float и overflow я высказал предположение ( http://habrahabr.ru/blogs/css/48429/ в конце), что overflow может положительно сказаться на производительности, потому что создаётся контекст форматирования, который ограничивает действия элементов самим собой. Ты как-то можешь это прокомментировать?
Спасибо, надо будет у себя поковыряться
Действительно непонятно, зачем ставить 10 мс задержки в setTimeout. Особенно учитывая, что в Firefox минимально возможная задержка в среднем 11 мс, а в IE аж 15 мс.
Зачастую тормоза пропадают при выставлении задержки в 30-50 мс, плавность анимации при этом не сильно страдает. Впрочем общий таймер на все анимации в любом случае полезен.
«В итоге в Firefox нагрузка на процессор снизилась… всего 10%».
До прочтения следующего абзаца кажется, что стало всего 10%, а не снизилась на 10%.
Спасибо, это, действительно, круто. Жду с нетерпением, когда у самого высвободится время, чтобы заняться таким же низкоуровневым профилированием 🙂
Это просто демонстрация
Там особо не от чего отвлекать, да и цифры бегают плавно. Суть анимации в том, чтобы показать, что происходит какая-то движуха, что время не просто указано, а неумолимо заканчивается.
Такие проблемы есть с инпользованием
-webkit-transform
,opacity
и флэша. Частичное решение проблемы я описывал тут.Может быть поможет, но сам iframe тоже не дешёвая штука. Надо тестировать.
Да, правильно. CSS3 тут приведён как пример сильно нагружающих процессор свойств. На бигбаззи их мало, но всё равно тормозит за счёт большого объёма информации. Я, например, частенько вместо
box-shadow
используюborder-image
с картинкой тени потому что работает намного быстрее.Я тоже так думал, но из демки видно, что это не так. Похоже, у Webkit гораздо более оптимизированный алгоритм определения областей перерисовки, у остальных это делается по каким-то вторичным признакам типа наличия определённых свойств у родителей.
А у Chrome это около 2 мс. В любом случае, простое снижение задержки не решает проблему в общем виде: браузер будет работать с той скоростью, с которой может.
вот и я говорю, что float надо остерегаться. круто, что я нашел подтверждение
Свойства задающие ширину по содержимому вроде float, display:inline-block или таблицы (кроме table-layout:fixed) неизбежно затратны по производительности из-за того, что они вынуждают рекурсивно обрабатывать содержимое.
В данном случае недоработка браузеров в том, что эта обработка происходит даже тогда, когда в принципе это можно избежать, зная что изменения происходят в абсолютно позиционированном содержимом, не влияющем на окружение. Нельзя назвать это багом, так как браузеры не обязаны проводить такую оптимизацию изначально (она была бы преждевременной), но доработать это поведение стоит.
В качестве хака решением для повышения производительности в данном случае был бы вынос счётчика повыше к <body> при помощи яваскрипта, но в таком случае возникают проблемы с изменением размеров окна браузера и резиновой вёрсткой, особенно когда есть минимальное и максимальное значения ширины.
А мне вот интересно — провел я исследование с помощью Web Inspector, нашел где Chrome делает лишние перерисовки… А в каком нибудь другом браузере получил обратный эффект т.к. там механизм инициирования перерисовки другой. Возможен такой ход событий или общие принципы у всех движков одинаковые?
Общие принципы у браузеров одинаковые. Я сомневаюсь, что если вы оптимизируете узкие места в Chrome, то в каком-нибудь Firefox будет работать медленнее. У разных браузеров может быть разный набор триггеров для reflow/repaint, и разные алгоритмы определения области перерисовки
получилось замкнуть перерисовку на .timer-wrap указав ему {position:absolute}
GreLI, большое спасибо за комментарий!
До сих пор не понимал почему динамические таблицы (table-layout:auto) так грузят браузер. Теперь буду начеку 🙂
Глеб, а данном случае это помогло, но на самом бигбаззи — нет. Всё равно repaint происходит на всём контейнере страницы
Спасибо! Основной урок для меня — проверять производительность скриптов на таймлайне. Не очень были понятны термины «reflow» и «repaint». (С термином «restyle» таких проблем не возникло, он обозначает изменение стилей элемента.) Откуда эти термины происходят?
reflow: пересчёт геометрии объектов (поменяли размер объекта — пересчитали размеры и координаты остальных)
repaint: перерисовка объектов и/или страницы
Еще раз спасибо. Но все же интересно откуда взялась эта терминология?
Сильно… Спасибо за еще одну очень полезную статью. Теперь я точно знаю почему так тормозит LookAtMe =)
Столкнулся с подобной штукой при разработке libcanvas.com. В Хроме (в Опере и Фоксе не наблюдалось) значительно падала скорость перерисовки содержимого Canvas на странице с тяжёлой версткой (помогало отключение position: relative родителю элемента canvas).
Спасибо за экстеншен, будет очень полезен.
поставьте на основной контейнер overflow:hidden — это отменит обтекание в большинстве браузеров и clear элементы не будут действовать на флоатнутый сайдбар. а для ie нужно поставить либо ширину, если можете, либо высоту. поскольку высоту в px не задашь, напишите height:1%. по идее должно помочь
overflow:hidden
— это первое, что я хотел сделать, но у контента слишком много выносных элементов: http://bigbuzzy.ru/catalog/christmastree/А display:inline-block не помогал?
В конце статьи:
Сергей, спасибо большое за материал, и отдельное спасибо за ссылку на статью Стояна Стефанова.
Очень познавательные у Вас статьи. Спасибо. так подробно и профессионально обзоры написаны, а главное от них польща!
именно в ff браузерные анимации всегда вызывали больше всего сложностей
спасибо за статью, перешлю сотрудникам
Замечательная статья, а главное все по полочкам разложено!
отличная статья! молодец!
Не знаю, кто это «мы», но вашему дизайнеру точно пора по голове настучать: http://oleg-istomin.livejournal.com/22864.html
А если вместо таймеров повесить заглушки, сам контент таймеров вынести в самое начало кода со стилями position:absolute, и по onLoad, onResize перемещать абсолютно-позиционируемые таймеры в смещения заглушек в документе? От влияния родителей по идее избавит
Интересная статья, спасибо.
В Хроме тоже проявляются подобные проблемы? Как я ни извращался, не смог нагрузить Хромом процессор больше, чем на 1%. Ваш сайт так же не нагрузил процессор (3%). То есть, лишние перерисовки есть, а нагрузки нет.
Проверял на слабеньком IdeaPad S10, win7, Chrome stable и Chrome dev 11.
Сергей, понимаю, что прошу сделаться экстрасенсом, но может подскажите в чем может быть проблема, я делаю сайт на котором будет навигация, похожая на http://www.mosgorreklama.ru/ , но у меня не перетаскиванием, а по принципу «куда мышь повел, туда экран и полетел», используются таймеры для анимации. Так вот, в Firefox 4, 3.6 присутствуют сильные лаги при перемещении слоя, может есть какие-то особенности движка, на которые стоит обратить внимание? Спасибо.